package pacman;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Timer;
import java.util.TimerTask;

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;

import maze.Coordinate;
import maze.Direction;
import maze.Location;
import maze.Maze;
import sensors.IRSensor;
import sensors.LightSensor;
import sensors.UltrasonicSensor;
import sensors.virtualSensors.VirtualInfraredSensor;
import sensors.virtualSensors.VirtualLightSensor;
import sensors.virtualSensors.VirtualUltrasonicSensor;
import Analyser.BarcodeAnalyser;
import behaviour.Explorer;
import behaviour.Strategy;

import communication.Communicator;
import communication.Config;

public class Ghost extends Robot {
	// De rotatie wordt genomen ten opzichte van de positieve x-as.
	private IRSensor infraredSensor;
	private LightSensor lightSensor;
	private UltrasonicSensor ultrasonicSensor;
	public Strategy strategy;
	private int sensorRotation; //absolute rotation tov het veld 
	public BarcodeAnalyser barcodeAnalyser = new BarcodeAnalyser();
	private boolean pacmanFound;
	private ArrayList<String> barcodeRead;
	/**
	 * The name of this player/robot
	 */
	private String name;
	
	/**
	 * True if this player thinks he captured pacman
	 */
	private boolean captured;
	
	/**
	 * The list of sectors this player's robot will drive to
	 */
	private ArrayList<Location> plan;
	
	/**
	 * Indicates what version of the communication protocol this player uses.
	 */
	private String version;
	
	/**
	 * The channel this player will use to communicate
	 */
	private Channel channel;
	
	public boolean terminated = false;
	public boolean isTravelling = false; //should only be set from within the simulator
	
	/**
	 * Initializes a new Robot located at a given location
	 * 
	 * @param locs
	 * 			The location for this Robot
	 * @param maze
	 * 			The maze where this robot will ride around
	 * @param infraredSensor
	 * 			The infraredSensor this robot will be using\
	 * @param lightSensor
	 * 			The lightSensor this robot will be using
	 * @param ultrasonicSensor
	 * 			The ultrasonicSensor this robot will be using
	 * @throws IOException 
	 */
	public Ghost(Location loc, int rotation, IRSensor infraredSensor, LightSensor lightSensor, UltrasonicSensor ultrasonicSensor, String name) throws IOException{
		this.setMaze(new Maze(10));
		this.setLocation(loc);
		this.setRotation(rotation);
		this.sensorRotation=rotation;
		barcodeAnalyser.whiteMargin = 40;
		barcodeAnalyser.blackMargin = 40;
		this.setInfraredSensor(infraredSensor);
		this.setLightSensor(lightSensor);
		this.setUltrasonicSensor(ultrasonicSensor);
		this.strategy=new Explorer(this.getMaze());
		this.barcodeRead = new ArrayList<String>();
		this.setName(name);
		
		ConnectionFactory factory = new ConnectionFactory();
        factory.setHost(Config.HOST_NAME);
        Connection connection = factory.newConnection();
        this.channel = connection.createChannel();

        this.channel.exchangeDeclare(Config.EXCHANGE_NAME, "fanout");
	}
	
	/**
	 * Initializes a new Robot with virtual sensors
	 * 
	 * @param loc
	 * 			The location for this Robot
	 * @param maze
	 * 			The maze where this robot will ride around
	 * @throws IOException 
	 */
	public Ghost(Location loc, int rotation, String name) throws IOException{
		this.setMaze(new Maze(10));
		this.setLocation(loc);
		this.setRotation(rotation);
		this.sensorRotation=90;
		this.addVirtualSensors();
		//this.strategy=new Explorer(loc, this.getMaze());
		this.barcodeRead = new ArrayList<String>();
		this.setName(name);
		ConnectionFactory factory = new ConnectionFactory();
        factory.setHost(Config.HOST_NAME);
        Connection connection = factory.newConnection();
        this.channel = connection.createChannel();

        this.channel.exchangeDeclare(Config.EXCHANGE_NAME, "fanout");
	}
	
	public Direction[] shiftArray(Direction[] directions, int amount){
		for (int j = 0; j < amount; j++) {
			Direction a = directions[directions.length - 1];
            int i;
            for (i = directions.length - 1; i > 0; i--)
            	directions[i] = directions[i - 1];
            directions[i] = a;
		}
		return directions;
	}
	
	public void run()
	{
			
		Timer timer = new Timer();
		
		BarcodeAnalyserTask barcodeAnalyserTask = new BarcodeAnalyserTask();
		int barcodeUpdatePeriod = 10; //ms (was 10)		
		
		timer.schedule(barcodeAnalyserTask, 0, barcodeUpdatePeriod);
		
		while(strategy != null)
		{
			if(!isTravelling)
				updateStrategy();
		}
		
	}
	
	private void updateStrategy()
	{
		
		Maze maze= this.getMaze();
		Coordinate currentCoordinate=maze.getCoordinateOfLocation(this.getLocation());
		int pacmanDirection=this.infraredSensor.getDirection();
		if(pacmanDirection!=0){
			pacmanFound=true;
		}
		Direction[] directions= new Direction[4];
		directions[0]=Direction.NORTH;
		directions[1]=Direction.EAST;
		directions[2]=Direction.SOUTH;
		directions[3]=Direction.WEST;
		
		//Gebruikt BARCODE commando richtingen
		int direction = generateDirection();
		if(direction == 1){ //Kijkt nr zuiden
			shiftArray(directions, 2);
		}else if(direction == 2){ //Kijkt naar westen
			shiftArray(directions, 1);
		}else if(direction == 4){ //Kijkt naar oosten
			shiftArray(directions, 3);
		}
		if(!maze.hasThisSectorExplored(currentCoordinate)){
			try {
				exploreSector(maze, currentCoordinate,directions);
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
		if(pacmanFound&&maze.pathToClosestUnknown(0,0,true)==null){
			//gewonnen
			System.out.println("we won!");
			strategy=null;
			if(this.getCommunicator()!=null)
				this.getCommunicator().terminate();
			terminated = true;
			return;
		}
		Direction next= strategy.nextDirection(getLocation());
		Direction current=directions[0];
		if(next==current.left())
		{
			this.rotate(90);
		}
		else if(next==current.opposite())
		{
			this.rotate(-180);
		}
		else if(next==current.right()){
			this.rotate(-90);
		}
		if(maze.read(currentCoordinate.getX(), currentCoordinate.getY(), next.left(), true)==1){
			travelToNextSector(true);
//			this.travel(40);
		}
		else if(maze.read(currentCoordinate.getX(), currentCoordinate.getY(), next.right(), true)==1){
			travelToNextSector(false);
//			this.travel(40);
		}
		else{
			this.travel(40);
		}
	}
	
	public void travelToNextSector(boolean wallLeft){
		int sign = wallLeft ? -1 : 1;
		rotateSensorMotor(sign*90);
		int begin=ultrasonicSensor.getDistance();
		if(begin==0)
			begin=ultrasonicSensor.getDistance();
		travel(40);
		int end=this.ultrasonicSensor.getDistance();
		rotateSensorMotor(-sign*90);
		if(Math.abs(begin-end)>7){
			int angle=Math.round(getAngle(wallLeft,begin,end));
			this.rotate(angle);
		}
	}
	private float getAngle(boolean left,int begin, int end){
		int sign = left ? 1 : -1;
		double v = Math.abs(begin-end);
		float angle = (float) Math.atan(v/40); //Nog in radians	
		angle *= 180/Math.PI;
		if(begin > end){
			angle *= -1;
		}
		return sign*angle; //In graden
	}
	
	private Direction[] exploreSector(Maze maze, Coordinate currentCoordinate, Direction[] directions) throws IOException {
		int i=0;
		int [] walls = new int[4];
		while(i<4){
			/**
			 * Een waarde van 0 duidt aan dat er geen obstakel
			aanwezig is. De waarde 1 duidt op een obstakel . Ten slotte
			duidt de waarde 2 er op dat de noordzijde niet is onderzocht.
			 */
			int distance=ultrasonicSensor.getDistance();
			if(distance<23){
				maze.write(currentCoordinate.getX(),currentCoordinate.getY() ,directions[i], (char) 1, true);
				walls[i] = 1;
				if(distance<13){
					int rotationToCorrect=this.getSensorRotation()-this.getRotation();
					if(rotationToCorrect<-180)
						rotationToCorrect+=360;
					else if(rotationToCorrect>180)
						rotationToCorrect-=360;
					this.rotate(rotationToCorrect);
					this.travel(distance-13);
					this.rotate(-rotationToCorrect);
				}
			}
			//nog geen rekening gehouden dat 255 errorwaarde kan zijn
			else{
				maze.write(currentCoordinate.getX(),currentCoordinate.getY() ,directions[i], (char) 2, true);
				walls[i] = 0;
			}
			rotateSensorMotor(90);
			i++;
		}
		
		/**
		 * String message = "JOIN\n";
			channel.basicPublish("ORANJE", "", null, message.getBytes());
		 */
		int [] nozw = new int [4];
		nozw = generateInformation(walls, directions);
		String message = "" + getName() + " DISCOVER ";
		//[p:coord] [n:trit] [o:trit] [z:trit] [w:trit]\n\n
		message += getMaze().getCoordinateOfLocation(getLocation()).toString();
		for(int j = 0; j < 4; j++){
			message += " " + nozw[j];
		}
		message+= "\n";
		channel.basicPublish(Config.EXCHANGE_NAME, "", null, message.getBytes());
		rotateSensorMotor(-360);
		return directions;
	}
	public void rotate(int degrees){
		super.rotate(degrees);
		this.sensorRotation+=degrees;
	}
	private int[] generateInformation(int[] walls, Direction[] directions) {
		int [] nozw = new int [4];
		int indexNorth = -1;
		for(int i = 0; i < walls.length; i++){
			if(directions[i] == Direction.NORTH){
				indexNorth = i;
			}
		}
		switch(indexNorth){
		case(0): return walls;
		case(1): return shiftArray(walls, 3);
		case(2): return shiftArray(walls, 2);
		case(3): return shiftArray(walls, 1);
		}
		return nozw;
	}

	private int[] shiftArray(int[] walls, int amount) {
		for (int j = 0; j < amount; j++) {
			int a = walls[walls.length - 1];
            int i;
            for (i = walls.length - 1; i > 0; i--)
            	walls[i] = walls[i - 1];
            walls[i] = a;
		}
		return walls;
	}

	public void rotateSensorMotor(int degrees){
		Communicator comm=this.getCommunicator();
		if(comm!=null)
		{
			comm.rotateSensorMotor(-degrees);
			//sleep(70);
		}
		
		sensorRotation-=degrees;
			
	}
	
	private void sleep(int milli)
	{
		try {
			Thread.sleep(milli);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
	public int getSensorRotation(){
		return this.sensorRotation;
	}
	
	/**
	 * Call this method if and only if this ghost is a real one.
	 */
	public void SetupCommunicator()
	{
		if(getCommunicator() == null)
			setCommunicator(new Communicator());				
	}	
	
	/**
	 * Add all the virtual sensors to the robot
	 */
	public void addVirtualSensors(){
		VirtualLightSensor lightSensor = new VirtualLightSensor(this);
		this.setLightSensor(lightSensor);
		VirtualInfraredSensor infraredSensor = new VirtualInfraredSensor(this);
		this.setInfraredSensor(infraredSensor);
		VirtualUltrasonicSensor ultrasonicSensor = new VirtualUltrasonicSensor(this);
		this.setUltrasonicSensor(ultrasonicSensor);
	}

	public IRSensor getInfraredSensor() {
		return infraredSensor;
	}

	public void setInfraredSensor(IRSensor infraredSensor) {
		this.infraredSensor = infraredSensor;
	}

	public LightSensor getLightSensor() {
		return lightSensor;
	}

	public void setLightSensor(LightSensor lightSensor) {
		this.lightSensor = lightSensor;
	}

	public UltrasonicSensor getUltrasonicSensor() {
		return ultrasonicSensor;
	}

	public void setUltrasonicSensor(UltrasonicSensor ultrasonicSensor) {
		this.ultrasonicSensor = ultrasonicSensor;
	}

	/**
	 * Invokes the Update() method of the BarcodeAnalyser every period when scheduled.
	 * Takes appropriate action when a barcode is detected.
	 */
	class BarcodeAnalyserTask extends TimerTask
	{
		@Override
		public void run() {
			if(!terminated)
			{	
				String bc = "";
				if(getCommunicator()==null){
					 bc = barcodeAnalyser.Update(lightSensor.readValue());
					
				}
				else
//					bc= getCommunicator().getBarcode();
				if(!bc.equals("")){
					System.out.println("Barcode detected: " +bc);
					barcodeRead.add(bc);
				}
					try {
						sendBARCODE(generateBarcodeCode(bc), generateDirection());
					} catch (IOException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
				}
			else
			{
				this.cancel();
			}			
		}		
	}
	
	/**
	 * Returns the code representation of a barcode.
	 * "Een binaire barcode van de vorm abcdef (waarbij elke
		letter een 1 of een 0 voorstelt)1 wordt voorgesteld door het gehele getal
		25 · a + 24 · b + 23 · c + 22 · d + 21 · e + 20 · f ."
	 * @param barcode
	 * @return
	 */
	public int generateBarcodeCode(String barcode){
		int code = 0;
		int macht = 0;
		for(int i = (barcode.length()-1); i >= 0 ; i--){
			int waarde = Character.getNumericValue(barcode.charAt(i));
			code += waarde*Math.pow(2, macht);
			macht++;
		}		
		return code;
	}
	
	/**
	 * Returns the integer value of this Ghost's direction.
	 * "Hierbij wordt de waarde 
	 * 1 gebruikt voor noord naar zuid,  == ZUID
	 * 2 voor oost naar west, 			 == WEST
	 * 3 voor zuid naar noord, 			 == NOORD
	 * 4 voor west naar oost."			 == OOST
	 */
	public int generateDirection(){
		int rot = this.getRotation()%360;
		rot = rot<0?(360-Math.abs(rot)):rot;
		int direction = -1;
		if(rot > 45 && rot < 135){
			direction = 3;
		}else if(rot >= 135 && rot < 225){
			direction = 2;
		}else if(rot >= 225 && rot < 315){
			direction = 1;
		}else{
			direction = 4;
		}
		return direction; 
	}
	
	/**
	 * Processes the POSITION command
	 * 
	 * @param location
	 * 			The new location of this robot in his maze.
	 */
	public void processPOSITION(Location location) {
		setLocation(location);
	}
	
	/**
	 * Processes the DISCOVER command
	 */
	public void processDISCOVER(Location loc, int north, int east,	int south, int west) {
		
		getMaze().write((int) loc.getX(), (int) loc.getY(), maze.Direction.NORTH, (char) north, true);
		getMaze().write((int) loc.getX(), (int) loc.getY(), maze.Direction.EAST, (char) east, true);
		getMaze().write((int) loc.getX(), (int) loc.getY(), maze.Direction.SOUTH, (char) south, true);
		getMaze().write((int) loc.getX(), (int) loc.getY(), maze.Direction.WEST, (char) west, true);
	}

	/**
	 * Processes the CAPTURED command
	 *
	 */
	public void processCAPTURED() {
		this.setCaptured(true);
	}
	
	/**
	 * Processes the PLAN command.
	 */
	public void processPLAN(ArrayList<Location> plan) {
		this.setPlan(plan);		
	}
	
	/**
	 * Processes the CANCELPLAN command.
	 */
	public void processCANCELPLAN() {
		this.setPlan(null);
		
	}
	
	/**
	 * Processes the PING command
	 * @throws IOException 
	 */
	public void processPING(String sender, String message) throws IOException{
		sendPONG(sender, message);
	}
	

	/**
	 * Processes the PONG command
	 */
	public void processPONG(String message) {
		//Ja wat doe je hier mee? 
	}
	
	public void sendJOIN() throws IOException{
		String message = "JOIN\n";
		channel.basicPublish(Config.EXCHANGE_NAME, "", null, message.getBytes());
	}
	
	public void sendNAME() throws IOException{
		String message = getName() + " JOIN " + "1.0\n";
		channel.basicPublish(Config.EXCHANGE_NAME, "", null, message.getBytes());
	}
	
	public void sendPOSITION() throws IOException{
		String message = getName() + " POSITION "
									+ getLocation().toString()
									+ "\n";
		channel.basicPublish(Config.EXCHANGE_NAME, "", null, message.getBytes());
	}
	
	public void sendDISCOVER(int n, int o, int z, int w) throws IOException{
		String message = getName() 	+ " DISCOVER " 
									+ getLocation().toString()
									+ " " + n 
									+ " " + o
									+ " " + z
									+ " " + w
									+ "\n";
		channel.basicPublish(Config.EXCHANGE_NAME, "", null, message.getBytes());	
	}
	
	public void sendBARCODE(int code, int direction) throws IOException{
		String message = getName() + " BARCODE "
									+ code
									+ " " + direction
									+ "\n";
		channel.basicPublish(Config.EXCHANGE_NAME, "", null, message.getBytes());	

 	}
	
	public void sendPACMAN(Location loc) throws IOException{
		String message = getName() + " PACMAN "
				+ loc.toString() 
				+ "\n";
		channel.basicPublish(Config.EXCHANGE_NAME, "", null, message.getBytes());	
	}
	
	public void sendCAPTURED() throws IOException{
		String message = getName() + " CAPTURED\n";
		channel.basicPublish(Config.EXCHANGE_NAME, "", null, message.getBytes());	
	}
	
	public void sendPLAN(ArrayList<Location> plan) throws IOException{
		String message = getName() + " PLAN";
		for(Location loc: plan){
			message += " " + loc.toString();
		}
		message += "\n";
		channel.basicPublish(Config.EXCHANGE_NAME, "", null, message.getBytes());	
	}
	
	public void sendCANCELPLAN() throws IOException{
		String message = getName() + " CANCELPLAN\n";
		channel.basicPublish(Config.EXCHANGE_NAME, "", null, message.getBytes());	
	}
	
	public void sendPING(String destination, String string) throws IOException{
		String message = getName() + " PING "
									+ destination
									+ " " + string
									+ "\n";
		channel.basicPublish(Config.EXCHANGE_NAME, "", null, message.getBytes());	
	}
	
	public void sendPONG(String destination, String string) throws IOException{
		String message = getName() + " PONG "
									+ destination
									+ " " + string
									+ "\n";
		channel.basicPublish(Config.EXCHANGE_NAME, "", null, message.getBytes());	
	}
	
	public String getName() {
		return name;
	}

	private void setName(String name) {
		this.name = name;
	}
	
	public boolean hasCaptured() {
		return captured;
	}

	private void setCaptured(boolean captured) {
		this.captured = captured;
	}
	
	public ArrayList<Location> getPlan() {
		return plan;
	}

	private void setPlan(ArrayList<Location> plan) {
		this.plan = plan;
	}
	
	public String getVersion() {
		return version;
	}

	private void setVersion(String version) {
		this.version = version;
	}
	
	public Channel getChannel() {
		return channel;
	}

	private void setChannel(Channel channel) {
		this.channel = channel;
	}
	public ArrayList<String> getBarcodesRead(){
		return this.barcodeRead;
	}
}

